Backport path_startswith_full()
authorArnaud Rebillout <arnaudr@debian.org>
Mon, 6 Apr 2026 11:17:22 +0000 (18:17 +0700)
committerArnaud Rebillout <arnaudr@debian.org>
Mon, 13 Apr 2026 07:18:40 +0000 (14:18 +0700)
This is a prerequisite to fix CVE-2026-29111.

path_startswith_full() was introduced in systemd v249, in commit 63f11e354a3:
"path-util: use path_find_first_component() in path_startswith()".

Looking at the commit, we can see that the existing path_startswith() function
became a special-case of path_startswith_full(), but there's more to it.
path_startswith_full() is also a complete rewrite of the original
path_startswith(), and the commit message mentions that the new implementation
is stricter.

To avoid surprises and potential regressions, this commit opts for a
conservative approach: we don't touch the existing path_startswith() function,
and we add path_startswith_full() as a entirely new function.

Note that it's enough for our purpose: the fix for CVE-2026-29111 makes use of
path_startswith_full().

path_startswith_full() was updated after it was introduced in v249: indeed, it
was extended to address CVE-2026-29111, and the change was backported to v257.
Therefore, this commits takes the function (and associated unit tests) from the
v257 branch.

Forwarded: not-needed

Gbp-Pq: Name CVE-2026-29111-2.patch

src/basic/path-util.c
src/basic/path-util.h
src/test/test-path-util.c

index d6e66970fe3ccf91d83c6cabd9c9ce6467c58590..41814297aa0823f964c5e76175a7994dec393e57 100644 (file)
@@ -430,6 +430,63 @@ int path_simplify_and_warn(
         return 0;
 }
 
+char* path_startswith_full(const char *original_path, const char *prefix, PathStartWithFlags flags) {
+        assert(original_path);
+        assert(prefix);
+
+        /* Returns a pointer to the start of the first component after the parts matched by
+         * the prefix, iff
+         * - both paths are absolute or both paths are relative,
+         * and
+         * - each component in prefix in turn matches a component in path at the same position.
+         * An empty string will be returned when the prefix and path are equivalent.
+         *
+         * Returns NULL otherwise.
+         */
+
+        const char *path = original_path;
+
+        if ((path[0] == '/') != (prefix[0] == '/'))
+                return NULL;
+
+        for (;;) {
+                const char *p, *q;
+                int m, n;
+
+                m = path_find_first_component(&path, !FLAGS_SET(flags, PATH_STARTSWITH_REFUSE_DOT_DOT), &p);
+                if (m < 0)
+                        return NULL;
+
+                n = path_find_first_component(&prefix, !FLAGS_SET(flags, PATH_STARTSWITH_REFUSE_DOT_DOT), &q);
+                if (n < 0)
+                        return NULL;
+
+                if (n == 0) {
+                        if (!p)
+                                p = path;
+
+                        if (FLAGS_SET(flags, PATH_STARTSWITH_RETURN_LEADING_SLASH)) {
+
+                                if (p <= original_path)
+                                        return NULL;
+
+                                p--;
+
+                                if (*p != '/')
+                                        return NULL;
+                        }
+
+                        return (char*) p;
+                }
+
+                if (m != n)
+                        return NULL;
+
+                if (!strneq(p, q, m))
+                        return NULL;
+        }
+}
+
 char* path_startswith(const char *path, const char *prefix) {
         assert(path);
         assert(prefix);
index af82624e489c397afebd441c1b24f6d62043f35d..a1098311713dea55baf4c12122cf1c9f774849c0 100644 (file)
@@ -57,6 +57,13 @@ char* path_make_absolute(const char *p, const char *prefix);
 int safe_getcwd(char **ret);
 int path_make_absolute_cwd(const char *p, char **ret);
 int path_make_relative(const char *from_dir, const char *to_path, char **_r);
+
+typedef enum PathStartWithFlags {
+        PATH_STARTSWITH_REFUSE_DOT_DOT       = 1U << 0,
+        PATH_STARTSWITH_RETURN_LEADING_SLASH = 1U << 1,
+} PathStartWithFlags;
+
+char* path_startswith_full(const char *path, const char *prefix, PathStartWithFlags flags) _pure_;
 char* path_startswith(const char *path, const char *prefix) _pure_;
 int path_compare(const char *a, const char *b) _pure_;
 bool path_equal(const char *a, const char *b) _pure_;
index 699aacefd459826dac38e20c9b87a3c1b0143420..9f332e39d679572f236b1c71d5af2fb7ae0fbd40 100644 (file)
@@ -444,6 +444,22 @@ static void test_path_startswith(void) {
         assert_se(!path_startswith("/foo/bar/barfoo/", "/f/b/b/"));
 }
 
+static void test_path_startswith_return_leading_slash_one(const char *path, const char *prefix, const char *expected) {
+        const char *p;
+
+        log_debug("/* %s(%s, %s) */", __func__, path, prefix);
+
+        p = path_startswith_full(path, prefix, PATH_STARTSWITH_RETURN_LEADING_SLASH);
+        assert_se(streq_ptr(p, expected));
+}
+
+static void test_path_startswith_return_leading_slash(void) {
+        test_path_startswith_return_leading_slash_one("/foo/bar", "/", "/foo/bar");
+        test_path_startswith_return_leading_slash_one("/foo/bar", "/foo", "/bar");
+        test_path_startswith_return_leading_slash_one("/foo/bar", "/foo/bar", NULL);
+        test_path_startswith_return_leading_slash_one("/foo/bar/", "/foo/bar", "/");
+}
+
 static void test_prefix_root_one(const char *r, const char *p, const char *expected) {
         _cleanup_free_ char *s = NULL;
         const char *t;
@@ -808,6 +824,7 @@ int main(int argc, char **argv) {
         test_make_relative();
         test_strv_resolve();
         test_path_startswith();
+        test_path_startswith_return_leading_slash();
         test_prefix_root();
         test_file_in_same_dir();
         test_path_find_first_component();